/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2022. All rights reserved.
 */

import fs from 'fs';
import os from 'os';
import path from 'path';

import { JudgeUtil } from './judge-util.js';

enum ErrorType {
  Char,
  EOF,
  Identifier,
}

type Token = { type: string; value: any; line: number; column: number };

let parseState = 'start';
let stack: { [index: string]: any }[] = [];
let pos = 0;
let line = 1;
let column = 0;
let token: Token;
let source: string;
let key: string;
let root: any;
let needLocation = false;
let lexState = 'default';
let buffer: string;
let quotationMark = "'";
let sign = 1;
let c: string | undefined;

/**
 * 解析JSON/JSON5文件
 *
 * @param filePath json文件路径
 * @param location 是否携带位置信息
 * @param encoding
 */
export function parseJsonFile(filePath: string, location = false, encoding = 'utf-8'): any {
  const text = fs.readFileSync(path.resolve(filePath), { encoding: encoding as BufferEncoding });

  try {
    return parseJsonText(text, location);
  } catch (e) {
    if (e instanceof SyntaxError) {
      const msg = e.message.split('at');
      if (msg.length === 2) {
        throw new Error(`${msg[0].trim()}${os.EOL}\t at ${filePath}:${msg[1].trim()}`);
      }
    }
    throw new Error(`${filePath} is not in valid JSON/JSON5 format.`);
  }
}

/**
 * 解析JSON/JSON5字符串
 *
 * @param text json字符串
 * @param location 是否携带位置信息
 * @throws SyntaxError
 * @since 2022/6/17
 */
export function parseJsonText(text: string, location = false): any {
  source = String(text);
  parseState = 'start';
  stack = [];
  pos = 0;
  line = 1;
  column = 0;
  root = undefined;
  needLocation = location;

  do {
    token = lex();
    parseStates[parseState]();
  } while (token.type !== 'eof');

  return root;
}

function lex(): Token {
  lexState = 'default';
  buffer = '';
  quotationMark = "'";
  sign = 1;

  for (;;) {
    c = peek();

    const token = lexStates[lexState]();
    if (token) {
      return token;
    }
  }
}

function peek(): string | undefined {
  if (source[pos]) {
    return String.fromCodePoint(source.codePointAt(pos)!);
  }
  return undefined;
}

function read(): string | undefined {
  const c = peek();

  if (c === '\n') {
    line++;
    column = 0;
  } else if (c) {
    column += c.length;
  } else {
    column++;
  }

  if (c) {
    pos += c.length;
  }

  return c;
}

/**
 * 词法解析状态机
 * 读source构造token
 */
const lexStates: { [index: string]: () => Token | void } = {
  default() {
    switch (c) {
      case '/':
        read();
        lexState = 'comment';
        return;

      case undefined:
        read();
        return newToken('eof');
    }

    if (JudgeUtil.isIgnoreChar(c) || JudgeUtil.isSpaceSeparator(c)) {
      read();
      return;
    }

    return lexStates[parseState]();
  },

  start() {
    lexState = 'value';
  },

  beforePropertyName() {
    switch (c) {
      case '$':
      case '_':
        buffer = read() as string;
        lexState = 'identifierName';
        return;

      case '\\':
        read();
        lexState = 'identifierNameStartEscape';
        return;

      case '}':
        return newToken('punctuator', read());

      case '"':
      case "'":
        quotationMark = c;
        read();
        lexState = 'string';
        return;
    }

    if (JudgeUtil.isIdStartChar(c)) {
      buffer += read();
      lexState = 'identifierName';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  afterPropertyName() {
    if (c === ':') {
      return newToken('punctuator', read());
    }

    throw error(ErrorType.Char, read());
  },

  beforePropertyValue() {
    lexState = 'value';
  },

  afterPropertyValue() {
    switch (c) {
      case ',':
      case '}':
        return newToken('punctuator', read());
    }

    throw error(ErrorType.Char, read());
  },

  beforeArrayValue() {
    if (c === ']') {
      return newToken('punctuator', read());
    }

    lexState = 'value';
    return undefined;
  },

  afterArrayValue() {
    switch (c) {
      case ',':
      case ']':
        return newToken('punctuator', read());
    }
    throw error(ErrorType.Char, read());
  },

  end() {
    throw error(ErrorType.Char, read());
  },

  /**
   * 处理注释的状态：comment
   * multiLineComment、singleLineComment
   * multiLineCommentAsterisk
   */
  comment() {
    switch (c) {
      case '*':
        read();
        lexState = 'multiLineComment';
        return;

      case '/':
        read();
        lexState = 'singleLineComment';
        return;
    }

    throw error(ErrorType.Char, read());
  },

  multiLineComment() {
    switch (c) {
      case '*':
        read();
        lexState = 'multiLineCommentAsterisk';
        return;

      case undefined:
        throw error(ErrorType.Char, read());
    }

    read();
  },

  multiLineCommentAsterisk() {
    switch (c) {
      case '*':
        read();
        return;

      case '/':
        read();
        lexState = 'default';
        return;

      case undefined:
        throw error(ErrorType.Char, read());
    }

    read();
    lexState = 'multiLineComment';
  },

  singleLineComment() {
    switch (c) {
      case '\n':
      case '\r':
      case '\u2028':
      case '\u2029':
        read();
        lexState = 'default';
        return;

      // 处理的是文件最后一行是单行注释的情况，可以正常结束。
      case undefined:
        read();
        return newToken('eof');
    }

    read();
    return undefined;
  },

  value() {
    switch (c) {
      case '{':
      case '[':
        return newToken('punctuator', read());

      case 'n':
        read();
        literal('ull');
        return newToken('null', null);

      case 't':
        read();
        literal('rue');
        return newToken('boolean', true);

      case 'f':
        read();
        literal('alse');
        return newToken('boolean', false);

      case '-':
      case '+':
        if (read() === '-') {
          sign = -1;
        }
        lexState = 'numerical';
        return;

      case '.':
      case '0':
      case 'I':
      case 'N':
        lexState = 'numerical';
        return;

      case '"':
      case "'":
        quotationMark = c;
        read();
        buffer = '';
        lexState = 'string';
        return;
    }

    if (c !== undefined && JudgeUtil.isDigitWithoutZero(c)) {
      lexState = 'numerical';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  /**
   * 处理number的状态：numerical
   * zero、decimalInteger、decimalPointLeading
   * hexadecimal、decimal、decimalFraction
   * hexadecimalInteger、decimalExponent、decimalExponentSign、decimalExponentInteger
   */
  numerical() {
    switch (c) {
      case '.':
        buffer = read() as string;
        lexState = 'decimalPointLeading';
        return;

      case '0':
        buffer = read() as string;
        lexState = 'zero';
        return;

      case 'I':
        read();
        literal('nfinity');
        return newToken('numeric', sign * Infinity);

      case 'N':
        read();
        literal('aN');
        return newToken('numeric', NaN);
    }

    if (c !== undefined && JudgeUtil.isDigitWithoutZero(c)) {
      buffer = read() as string;
      lexState = 'decimalInteger';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  zero() {
    switch (c) {
      case '.':
      case 'e':
      case 'E':
        lexState = 'decimal';
        return;

      case 'x':
      case 'X':
        buffer += read();
        lexState = 'hexadecimal';
        return;
    }

    return newToken('numeric', 0);
  },

  decimalInteger() {
    switch (c) {
      case '.':
      case 'e':
      case 'E':
        lexState = 'decimal';
        return;
    }

    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      return;
    }

    return newToken('numeric', sign * Number(buffer));
  },

  decimal() {
    switch (c) {
      case '.':
        buffer += read();
        lexState = 'decimalFraction';
        break;

      case 'e':
      case 'E':
        buffer += read();
        lexState = 'decimalExponent';
        break;
    }
  },

  decimalPointLeading() {
    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      lexState = 'decimalFraction';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  decimalFraction() {
    switch (c) {
      case 'e':
      case 'E':
        buffer += read();
        lexState = 'decimalExponent';
        return;
    }

    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      return;
    }

    return newToken('numeric', sign * Number(buffer));
  },

  decimalExponent() {
    switch (c) {
      case '+':
      case '-':
        buffer += read();
        lexState = 'decimalExponentSign';
        return;
    }

    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      lexState = 'decimalExponentInteger';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  decimalExponentSign() {
    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      lexState = 'decimalExponentInteger';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  decimalExponentInteger() {
    if (JudgeUtil.isDigit(c)) {
      buffer += read();
      return;
    }

    return newToken('numeric', sign * Number(buffer));
  },

  hexadecimal() {
    if (JudgeUtil.isHexDigit(c)) {
      buffer += read();
      lexState = 'hexadecimalInteger';
      return;
    }

    throw error(ErrorType.Char, read());
  },

  hexadecimalInteger() {
    if (JudgeUtil.isHexDigit(c)) {
      buffer += read();
      return;
    }

    return newToken('numeric', sign * Number(buffer));
  },

  /**
   * 处理标识符，key值：identifierNameStartEscape
   * identifierName、identifierNameEscape
   */
  identifierNameStartEscape() {
    if (c !== 'u') {
      throw error(ErrorType.Char, read());
    }

    read();
    const u = unicodeEscape();
    switch (u) {
      case '$':
      case '_':
        break;

      default:
        if (!JudgeUtil.isIdStartChar(u)) {
          throw error(ErrorType.Identifier);
        }
        break;
    }

    buffer += u;
    lexState = 'identifierName';
  },

  identifierName() {
    switch (c) {
      case '$':
      case '_':
      case '\u200C':
      case '\u200D':
        buffer += read();
        return;

      case '\\':
        read();
        lexState = 'identifierNameEscape';
        return;
    }

    if (JudgeUtil.isIdContinueChar(c)) {
      buffer += read();
      return;
    }

    return newToken('identifier', buffer);
  },

  identifierNameEscape() {
    if (c !== 'u') {
      throw error(ErrorType.Char, read());
    }

    read();
    const u = unicodeEscape();
    switch (u) {
      case '$':
      case '_':
      case '\u200C':
      case '\u200D':
        break;

      default:
        if (!JudgeUtil.isIdContinueChar(u)) {
          throw error(ErrorType.Identifier);
        }
        break;
    }

    buffer += u;
    lexState = 'identifierName';
  },

  /**
   * 处理字符串的状态
   */
  string() {
    switch (c) {
      case '\\':
        read();
        buffer += escape();
        return;

      case '"':
      case "'":
        if (c === quotationMark) {
          const strToken = newToken('string', buffer);
          read();
          return strToken;
        }

        buffer += read();
        return;

      case '\n':
      case '\r':
      case undefined:
        throw error(ErrorType.Char, read());

      case '\u2028':
      case '\u2029':
        separatorChar(c);
        break;
    }

    buffer += read();
    return undefined;
  },
};

function newToken(type: string, value?: any) {
  return {
    type,
    value,
    line,
    column,
  };
}

/**
 * 匹配特定字符串
 *
 * @param s
 */
function literal(s: string) {
  for (const c of s) {
    const p = peek();

    if (p !== c) {
      throw error(ErrorType.Char, read());
    }

    read();
  }
}

function escape() {
  const c = peek();
  const controlLetter = controlEscape();
  if (controlLetter) {
    return controlLetter;
  }
  switch (c) {
    case '0':
      read();
      if (JudgeUtil.isDigit(peek())) {
        throw error(ErrorType.Char, read());
      }
      return '\0';

    case 'x':
      read();
      return hexEscape();

    case 'u':
      read();
      return unicodeEscape();

    case '\n':
    case '\u2028':
    case '\u2029':
      read();
      return '';

    case '\r':
      read();
      if (peek() === '\n') {
        read();
      }
      return '';
  }

  if (c === undefined || JudgeUtil.isDigitWithoutZero(c)) {
    throw error(ErrorType.Char, read());
  }

  return read();
}

function controlEscape() {
  const c = peek();
  switch (c) {
    case 'b':
      read();
      return '\b';

    case 'f':
      read();
      return '\f';

    case 'n':
      read();
      return '\n';

    case 'r':
      read();
      return '\r';

    case 't':
      read();
      return '\t';

    case 'v':
      read();
      return '\v';
  }
  return undefined;
}

function hexEscape() {
  let buffer = '';
  let c = peek();

  if (!JudgeUtil.isHexDigit(c)) {
    throw error(ErrorType.Char, read());
  }

  buffer += read();

  c = peek();
  if (!JudgeUtil.isHexDigit(c)) {
    throw error(ErrorType.Char, read());
  }

  buffer += read();

  return String.fromCodePoint(parseInt(buffer, 16));
}

function unicodeEscape() {
  let buffer = '';
  let count = 4;

  while (count-- > 0) {
    const c = peek();
    if (!JudgeUtil.isHexDigit(c)) {
      throw error(ErrorType.Char, read());
    }

    buffer += read();
  }

  return String.fromCodePoint(parseInt(buffer, 16));
}

/**
 * 语法解析状态机
 * 解析token，指定语法解析状态
 */
const parseStates: { [index: string]: Function } = {
  start() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    push();
  },

  beforePropertyName() {
    switch (token.type) {
      case 'identifier':
      case 'string':
        key = token.value;
        parseState = 'afterPropertyName';
        return;

      case 'punctuator':
        pop();
        return;

      case 'eof':
        throw error(ErrorType.EOF);
    }
  },

  afterPropertyName() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    parseState = 'beforePropertyValue';
  },

  beforePropertyValue() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    push();
  },

  afterPropertyValue() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    switch (token.value) {
      case ',':
        parseState = 'beforePropertyName';
        return;

      case '}':
        pop();
    }
  },

  beforeArrayValue() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    if (token.type === 'punctuator' && token.value === ']') {
      pop();
      return;
    }

    push();
  },

  afterArrayValue() {
    if (token.type === 'eof') {
      throw error(ErrorType.EOF);
    }

    switch (token.value) {
      case ',':
        parseState = 'beforeArrayValue';
        return;

      case ']':
        pop();
    }
  },

  end() {
    // 表示结束状态
  },
};

function push() {
  const value = createValue();

  if (needLocation && typeof value === 'object') {
    value['_line'] = line;
    value['_column'] = column;
  }
  if (root === undefined) {
    root = value;
  } else {
    const parent = stack[stack.length - 1];
    if (Array.isArray(parent)) {
      if (needLocation && typeof value !== 'object') {
        parent.push({ value: value, _line: line, _column: column });
      } else {
        parent.push(value);
      }
    } else {
      if (needLocation && typeof value !== 'object') {
        parent[key] = { value: value, _line: line, _column: column };
      } else {
        parent[key] = value;
      }
    }
  }

  changeParseState(value);
}

function createValue() {
  let value;
  switch (token.type) {
    case 'punctuator':
      switch (token.value) {
        case '{':
          value = {};
          break;

        case '[':
          value = [];
          break;
      }
      break;

    case 'null':
    case 'boolean':
    case 'numeric':
    case 'string':
      value = token.value;
      break;
  }
  return value;
}

function changeParseState(value: any) {
  if (value && typeof value === 'object') {
    stack.push(value);

    if (Array.isArray(value)) {
      parseState = 'beforeArrayValue';
    } else {
      parseState = 'beforePropertyName';
    }
  } else {
    const current = stack[stack.length - 1];
    if (!current) {
      parseState = 'end';
    } else if (Array.isArray(current)) {
      parseState = 'afterArrayValue';
    } else {
      parseState = 'afterPropertyValue';
    }
  }
}

function pop() {
  stack.pop();

  const current = stack[stack.length - 1];
  if (!current) {
    parseState = 'end';
  } else if (Array.isArray(current)) {
    parseState = 'afterArrayValue';
  } else {
    parseState = 'afterPropertyValue';
  }
}

function separatorChar(c: string) {
  console.warn(`JSON5: '${formatChar(c)}' in strings is not valid ECMAScript; consider escaping.`);
}

function formatChar(c: string) {
  const replacements: { [index: string]: string } = {
    "'": "\\'",
    '"': '\\"',
    '\\': '\\\\',
    '\b': '\\b',
    '\f': '\\f',
    '\n': '\\n',
    '\r': '\\r',
    '\t': '\\t',
    '\v': '\\v',
    '\0': '\\0',
    '\u2028': '\\u2028',
    '\u2029': '\\u2029',
  };

  if (replacements[c]) {
    return replacements[c];
  }

  if (c < ' ') {
    const hexString = c.charCodeAt(0).toString(16);
    return `\\x${`00${hexString}`.substring(hexString.length)}`;
  }

  return c;
}

function error(type: ErrorType, s?: string): SyntaxError {
  let errMsg = '';
  switch (type) {
    case ErrorType.Char:
      if (s === undefined) {
        errMsg = `JSON5: invalid end of input at ${line}:${column}`;
      } else {
        errMsg = `JSON5: invalid character '${formatChar(s)}' at ${line}:${column}`;
      }
      break;
    case ErrorType.EOF:
      errMsg = `JSON5: invalid end of input at ${line}:${column}`;
      break;
    case ErrorType.Identifier:
      column -= 5;
      errMsg = `JSON5: invalid identifier character at ${line}:${column}`;
      break;
  }

  const err = new MySyntaxError(errMsg);
  err.lineNumber = line;
  err.columnNumber = column;
  return err;
}

class MySyntaxError extends SyntaxError {
  lineNumber?: number;
  columnNumber?: number;
}
